﻿#region Copyright (C) 2008, 2009 StreamTv

/* 
 *      Copyright (C) 2008, 2009 StreamTv
 *      http://code.google.com/p/mpstreamtv/
 *
 *  This Program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *   
 *  This Program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *   
 *  You should have received a copy of the GNU General Public License
 *  along with GNU Make; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. 
 *  http://www.gnu.org/copyleft/gpl.html
 *
 */

#endregion

#region Imports
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using TvControl;
using TvLibrary.Interfaces;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel;
#endregion

namespace StreamTv2
{
    /// <summary>
    /// Small HTTP Server for streaming data to clients.
    /// </summary>
    public class SmallWebServer : IDisposable
    {
        #region Delegates
        private delegate void Process(ref HttpListenerContext clientContext);
        #endregion
        #region Variables
        // Private Variables
        //private const int numberOfListeningThreads = 5;
        private HttpListener listener;

        // Protected Variables
        // Public Variables
        #endregion
        #region Constructors/Destructors
        /// <summary>
        /// Constructor.
        /// </summary>
        public SmallWebServer()
        {
        } // SmallWebServer
        #endregion
        #region Public methods
        #region Start / Stop Server
        /// <summary>
        /// Start listening for connections.
        /// </summary>
        /// <param name="controller">TvServer controller.</param>
        public void Start(IController controller)
        {
            TvDatabase.TvBusinessLayer businessLayer = new TvDatabase.TvBusinessLayer();

            // Get settings.
            int httpPort = Convert.ToInt32(businessLayer.GetSetting("StreamTv.Http.Port", "8080").Value);
            
#if DEBUG
            httpPort = 9000;
#endif

            // Setup Http Server.
            listener = new HttpListener();
            listener.Prefixes.Add(String.Format("http://*:{0}/TvServer/", httpPort));
            
            listener.Start();

            // Load modules into the GlobalServiceProvider.
            LoadPlugins(controller);

            // Listen for clients.
            //listener.BeginGetContext(new AsyncCallback(ConnectionRecieved), listener);

            for (int i = 0; i < 1; i++)
            {
                ProcessClients(listener, (svr, exc) =>
                {
                    if (exc != null)
                    {
                        // HTTP Server crashed.
                        TvLibrary.Log.Log.Debug("StreamTv Http Server Shutdown, reason: {0}", exc.Message);
                    }
                });
            }
        } // Start

        /// <summary>
        /// Stop the http server.
        /// </summary>
        public void Stop()
        {
            listener.Stop();
            listener.Close();

            listener = null;
        } // Stop
        #endregion
        #endregion
        #region Private methods
        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="completed"></param>
        private static void ProcessClients(HttpListener server,
                                          Action<HttpListener, Exception> completed)
        {
            AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

            Action<Exception> done = e =>
            {
                if (completed != null) asyncOp.Post(delegate
                    {
                        completed(server, e);
                    }, null);
            };

            AsyncCallback rc = null;
            rc = connectionResult =>
            {
                try
                {
                    HttpListenerContext context = server.EndGetContext(connectionResult);
                    if (context != null)
                    {
                        String folders;
                        String file;
                        String extension;
                        String completePath;

                        SplitUrl(context.Request.Url, out folders, out file, out extension);

                        // Cheeky trick to get around no file given.
                        if (file == "")
                            file = "index.html";

                        // Check if the file exists.
                        Boolean exists = LocationExists(folders, file, extension, out completePath);

                        if (exists)
                        {
                            Boolean dataToSend = true;

                            int bufferSize = 0x1000;
                            Stream media = null;

                            IWebService service = null;
                            Boolean heartBeat = false;

                            RequestDetails details;

                            // Check is a module.
                            if (IsValidXml(completePath, context.Request.QueryString, context.Request.Url.Host, context.Request.Url.Port, out details))
                            {
                                // Check if there is a plugin registered to that module name.
                                if (PluginsProvider.GlobalPluginsProvider.Instance.IsRegistered(details.ModuleName))
                                {
                                    try
                                    {
                                        service = (IWebService)PluginsProvider.GlobalPluginsProvider.Instance.Get(details.ModuleName);

                                        string mime = "unknown/unknown";
                                        ErrorCode result = service.Process(context.Request.RequestTraceIdentifier.ToString(), details, out media, out mime);

                                        // Check if processing ran successfully.
                                        if (result != ErrorCode.Successful)
                                        {
                                            if (result == ErrorCode.NotFound)
                                                HttpResponses.NotFound(ref context);
                                            else
                                                HttpResponses.InternalServerError(ref context);

                                            dataToSend = false;
                                        }

                                        context.Response.ContentType = mime;

                                        heartBeat = service.HeatbeatRequired(context.Request.RequestTraceIdentifier.ToString());
                                        bufferSize = service.BufferSize(context.Request.RequestTraceIdentifier.ToString());
                                    }
                                    catch (Exception e)
                                    {
                                        // Problem with the module.
                                        TvLibrary.Log.Log.Debug("StreamTv - Plugin fault: {0} - Message: {1} - Stack Trace: {2}", new object[] { details.ModuleName, e.Message, e.StackTrace });

                                        HttpResponses.InternalServerError(ref context);
                                        dataToSend = false;

                                    }
                                }
                                else
                                {
                                    HttpResponses.NotFound(ref context);
                                    dataToSend = false;
                                }
                            }
                            else
                            {
                                // Send the contents of the file as is.
                                media = new FileStream(completePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                                context.Response.ContentType = MimeHelper.Mime.Get(extension);
                            }

                            if (dataToSend)
                            {
                                byte[] buffer = new byte[bufferSize];

                                if (heartBeat)
                                {
                                    CopyStreamToClient(media, context, service, context.Request.RequestTraceIdentifier.ToString(), bufferSize, (src, dst, svc, exc) =>
                                        {
                                            try
                                            {
#if DEBUG
                                                if(exc != null)
                                                    System.Diagnostics.Debug.WriteLine("Copy Stream Error: " + exc.Message);
#endif

                                                svc.ClientDisconnected(dst.Request.RequestTraceIdentifier.ToString());

                                                src.Close();
                                                dst.Response.OutputStream.Close();
                                            }
                                            catch (Exception)
                                            {
                                            }
                                        });
                                }
                                else
                                {
                                    CopyStreamToClient(media, context, bufferSize, (src, dst, exc) =>
                                    {
                                        try
                                        {
                                            src.Close();
                                            dst.Response.OutputStream.Close();
                                        }
                                        catch (Exception)
                                        {
                                        }
                                    });
                                }
                            }
                        }
                        else
                        {
                            HttpResponses.NotFound(ref context);
                        }
                    }

                    server.BeginGetContext(rc, null);
                }
                catch (Exception exc) { done(exc); }
            };

            server.BeginGetContext(rc, null);
        }

        /// <summary>
        /// Split the address into useful information.
        /// </summary>
        /// <param name="address">Complete address to phrase</param>
        /// <param name="folders">Folders in the address</param>
        /// <param name="file">Filename in the address</param>
        /// <param name="extension">File extension (if any)</param>
        private static void SplitUrl(Uri address, out String folders, out String file, out String extension)
        {
            if (address.IsFile)
            {
                folders = address.LocalPath.Remove(address.LocalPath.LastIndexOf('\\'));
                file = address.LocalPath.Substring(address.LocalPath.LastIndexOf('\\')).Remove(address.LocalPath.LastIndexOf('.'));
                extension = address.LocalPath.Substring(address.LocalPath.LastIndexOf('.'));
            }
            else
            {
                folders = address.AbsolutePath.Substring(
                    0,
                    address.AbsolutePath.LastIndexOf('/')
                );
                folders = folders.Replace("/", "\\");
                if (folders.ToLowerInvariant().StartsWith("\\tvserver"))
                    folders = folders.Substring(9); // Probably should just put in 9.

                if (address.AbsolutePath.Contains('.') & address.AbsolutePath.LastIndexOf('.') - address.AbsolutePath.LastIndexOf('/') - 1 >= 0)
                {
                    file = address.AbsolutePath.Substring(
                        address.AbsolutePath.LastIndexOf('/') + 1,
                        address.AbsolutePath.LastIndexOf('.') - address.AbsolutePath.LastIndexOf('/') - 1
                    );

                    extension = address.AbsolutePath.Substring(address.AbsolutePath.LastIndexOf('.') + 1);
                }
                else
                {
                    file = address.AbsolutePath.Substring(address.AbsolutePath.LastIndexOf('/') + 1);
                    extension = "";
                }
            }

            // Cheeky trick to get around no file given.
            if (file == "")
                file = "index.html";
        }

        /// <summary>
        /// Check if the location exists.
        /// </summary>
        /// <param name="folders">relative folder path to the location</param>
        /// <param name="file">file to lookup</param>
        /// <param name="extension">extension of the file</param>
        /// <returns>True if exists</returns>
        private static Boolean LocationExists(String folders, String file, String extension, out String completePath)
        {
            // Check if the file exists.
            String relativeFile;
            if (extension == "")
                relativeFile = String.Format(@"{0}\Team MediaPortal\MediaPortal TV Server\StreamTv\wwwroot{1}\{2}", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), folders, file);
            else
                relativeFile = String.Format(@"{0}\Team MediaPortal\MediaPortal TV Server\StreamTv\wwwroot{1}\{2}.{3}", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), folders, file, extension);

            completePath = relativeFile;

            return File.Exists(relativeFile);
        }

        private static Boolean IsValidXml(String location, System.Collections.Specialized.NameValueCollection query, String host, int port, out RequestDetails details)
        {
            try
            {
                RequestDetails result = new RequestDetails(query, XDocument.Load(location), host, port);
                details = result;
                return true;
            }
            catch (Exception)
            {
                details = null;
                return false;
            }
        }

        private static void CopyStreamToClient(Stream source, HttpListenerContext destination, int bufferSize,
                                               Action<Stream, HttpListenerContext, Exception> completed)
        {
            byte[] buffer = new byte[bufferSize];
            AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

            Action<Exception> done = e =>
            {
                if (completed != null) asyncOp.Post(delegate
                    {
                        completed(source, destination, e);
                    }, null);
            };

            AsyncCallback rc = null;
            rc = readResult =>
                {
                    try
                    {
                        int read = source.EndRead(readResult);
                        if (read > 0)
                        {
                            destination.Response.OutputStream.BeginWrite(buffer, 0, read, writeResult =>
                            {
                                try
                                {
                                    destination.Response.OutputStream.EndWrite(writeResult);
                                    source.BeginRead(
                                        buffer, 0, buffer.Length, rc, null);
                                }
                                catch (Exception exc) { done(exc); }
                            }, null);
                        }
                        else done(null);
                    }
                    catch (Exception exc) { done(exc); }
                };

            source.BeginRead(buffer, 0, bufferSize, rc, null);
        }

        private static void CopyStreamToClient(Stream source, HttpListenerContext destination, IWebService service, String clientId, int bufferSize,
                                               Action<Stream, HttpListenerContext, IWebService, Exception> completed)
        {
            byte[] buffer = new byte[bufferSize];
            AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

            Action<Exception> done = e =>
            {
                if (completed != null) asyncOp.Post(delegate
                {
                    completed(source, destination, service, e);
                }, null);
            };

            AsyncCallback rc = null;
            rc = readResult =>
            {
                try
                {
                    int read = source.EndRead(readResult);
                    if (read > 0)
                    {
                        destination.Response.OutputStream.BeginWrite(buffer, 0, read, writeResult =>
                        {
                            try
                            {
                                destination.Response.OutputStream.EndWrite(writeResult);

                                // Send heartbeat pulse to service.
                                service.HeartBeat(clientId);

                                source.BeginRead(
                                    buffer, 0, buffer.Length, rc, null);
                            }
                            catch (Exception exc) { done(exc); }
                        }, null);
                    }
                    else done(null);
                }
                catch (Exception exc) { done(exc); }
            };

            source.BeginRead(buffer, 0, bufferSize, rc, null);
        }

        private void LoadPlugins(IController controller)
        {
            try
            {
#if DEBUG
                List<String> possiblePlugins = Directory.GetFiles(String.Format(@"{0}\Plugins", AppDomain.CurrentDomain.BaseDirectory), "*.dll").ToList();

                possiblePlugins.RemoveAt(possiblePlugins.FindIndex(o => o.EndsWith("StreamTv2.dll")));
                possiblePlugins.RemoveAt(possiblePlugins.FindIndex(o => o.EndsWith("TvLibrary.Interfaces.dll")));
#else
                String[] possiblePlugins = Directory.GetFiles(String.Format(@"{0}\Team MediaPortal\MediaPortal TV Server\StreamTv\Plugins", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "*.dll"));
#endif

                foreach (string plugin in possiblePlugins)
                {
                    try
                    {

                        Assembly module;
                        module = Assembly.LoadFile(plugin);
                        Type[] moduleTypes = module.GetTypes();

                        if (module != null)
                        {
                            foreach (Type item in moduleTypes)
                            {
                                if (typeof(IWebService).IsAssignableFrom(item))
                                {
                                    IWebService service = Activator.CreateInstance(item) as IWebService;
                                    service.Initialize(controller);
                                    PluginsProvider.GlobalPluginsProvider.Instance.Add(service.Name, service);
                                }
                            }
                        }
                    }
#if DEBUG
                    catch (Exception e)
                    {
                        throw new Exception(String.Format("Error loading plugin: {0}", plugin), e);
                    }
#else
                catch (Exception)
                {
                    TvLibrary.Log.Log.Error("StreamTv: Error loading plugin: {0}", plugin);
                }
#endif
                }
            }
            catch (Exception e)
            {
                throw new Exception("Error loading plugins.", e);
            }
        }

        #endregion
        #region IDisposable Members
        /// <summary>
        /// Dispose of all resources.
        /// </summary>
        public void Dispose()
        {
            // Go through each module and dispose of it.
        }
        #endregion
    }
}
